﻿using System;
using System.Text;
using System.Linq;
using System.Reflection;
using System.Linq.Expressions;
using System.Collections.Generic;

using LeanEngine.Entity;
using LeanEngine.Data.Linq.Language;
using LeanEngine.Data.Linq.Extension;

namespace LeanEngine.Data.Linq.Translator
{
    internal sealed class WhereTranslator : LinqTranslator<WhereToken>
    {
        #region Declaration Members

        /// <summary>
        /// Represents the entire query context been evaluating
        /// </summary>
        private QueryContext _context;

        /// <summary>
        /// Represents the current lambda expression to be interpreted
        /// </summary>
        private LambdaExpression _lambda;

        /// <summary>
        /// Represent the where statement to be generated
        /// </summary>
        private WhereToken _whereStatement;

        /// <summary>
        /// Represent the current where filter been interpreted
        /// </summary>
        private CompareToken _currentFilter;

        /// <summary>
        /// Represent the stack of memberInfo expression to drill down annonymous members
        /// </summary>
        private Stack<MemberExpression> _memberStack;

        /// <summary>
        /// Represent the stack of combine operator
        /// </summary>
        private Stack<string> _binaryStack;

        #endregion

        #region Constructor Members

        public WhereTranslator()
        {
            _whereStatement = new WhereToken();
            _memberStack = new Stack<MemberExpression>();
            _binaryStack = new Stack<string>();
        }

        #endregion

        #region LinqTranslator<WhereToken> Members

        internal override bool CanEvaluate(QueryContext context)
        {
            switch (context.CurrentMethodCall.Method.Name)
            {
                case "Where": // is this really a proper Where?
                    _lambda = GetLambdaWithParamCheck(context.CurrentMethodCall);
                    return _lambda != null;

                default:
                    return false;
            }
        }

        internal override WhereToken Interpret(QueryContext context)
        {
            this._context = context;

            base.EvaluateLambda(_lambda);

            return _whereStatement;
        }

        #endregion

        #region Overrided Method Members

        protected override Expression EvaluateMethodCall(MethodCallExpression m)
        {
            Expression resultExpression = null;

            #region switch (currentMember.Method.Name)
            switch (m.Method.Name)
            {
                case "StartsWith":
                    _currentFilter = new StartWithToken();
                    resultExpression = InterpretLike(m);
                    break;
                case "EndsWith":
                    _currentFilter = new EndsWithToken();
                    resultExpression = InterpretLike(m);
                    break;
                case "Contains":
                    _currentFilter = new ContainsToken();
                    resultExpression = InterpretLike(m);
                    break;
                case "Coalesce":
                    return InterpretCoalesce(m);
                case "IsNotNull":
                case "IsNull":
                    _currentFilter = new CompareToken();
                    resultExpression = InterpretNull(m);
                    break;
                case "get_Item":
                    _currentFilter = new CompareToken();
                    resultExpression = InterpretGetItem(m);
                    break;
                //case "Replace":
                //case "Remove":
                //case "Insert":
                //case "ToLower":
                //case "ToString":
                //case "ToUpper":
                //case "Trim":
                //case "TrimEnd":
                //case "TrimStart":
                //case "Substring":
                default:
                    resultExpression = InterpretMethodResult(m);
                    return resultExpression;
            }
            #endregion

            if (this._currentFilter != null)
            {
                if (_binaryStack.Count > 0) this._currentFilter.CombineOperator = _binaryStack.Pop();
                this._whereStatement.PredicateList.Add(this._currentFilter);
                this._currentFilter = null;
                this._memberStack.Clear();
            }

            return resultExpression;
        }

        protected override Expression EvaluateUnary(UnaryExpression u)
        {
            #region switch (u.NodeType)
            switch (u.NodeType)
            {
                case ExpressionType.Not:
                    _currentFilter.Operator = CompareOperators.Not;
                    Evaluate(u.Operand);
                    break;

                case ExpressionType.Convert:
                    if (!u.IsLifted && !u.IsLiftedToNull && u.Method == null)
                    {
                        Evaluate(u.Operand);
                        break;
                    }
                    else
                    {
                        throw new NotSupportedException(string.Format("The unary operator '{0}' is not supported", u.NodeType));
                    }

                default:
                    throw new NotSupportedException(string.Format("The unary operator '{0}' is not supported", u.NodeType));
            }
            #endregion

            return u;
        }

        protected override Expression EvaluateBinary(BinaryExpression b)
        {
            if(_currentFilter == null) _currentFilter = new CompareToken();

            #region switch (b.NodeType)
            switch (b.NodeType)
            {
                case ExpressionType.And:
                case ExpressionType.AndAlso:
                    _currentFilter.CombineOperator = CompareOperators.AndAlso;
                    _binaryStack.Push(CompareOperators.AndAlso);
                    break;
                case ExpressionType.Or:
                case ExpressionType.OrElse:
                    _currentFilter.CombineOperator = CompareOperators.OrElse;
                    _binaryStack.Push(CompareOperators.OrElse);
                    break;
                case ExpressionType.Equal:
                    _currentFilter.Operator = b.IsComparingWithNull() ? CompareOperators.IsEquals : CompareOperators.Equals;
                    break;
                case ExpressionType.NotEqual:
                    _currentFilter.Operator = b.IsComparingWithNull() ? CompareOperators.IsEquals : CompareOperators.NotEquals;
                    break;
                case ExpressionType.GreaterThan:
                    _currentFilter.Operator = CompareOperators.GreaterThan;
                    break;
                case ExpressionType.GreaterThanOrEqual:
                    _currentFilter.Operator = CompareOperators.GreaterThanOrEqual;
                    break;
                case ExpressionType.LessThan:
                    _currentFilter.Operator = CompareOperators.LessThan;
                    break;
                case ExpressionType.LessThanOrEqual:
                    _currentFilter.Operator = CompareOperators.LessThanOrEqual;
                    break;
                default:
                    throw new NotSupportedException("The Where predicate does not support '" + b.NodeType + "' binary operator.");
            }
            #endregion

            Evaluate(b.Left);
            Evaluate(b.Right);

            if (this._currentFilter != null)
            {
                if(_binaryStack.Count > 0) this._currentFilter.CombineOperator = _binaryStack.Pop();
                this._whereStatement.PredicateList.Add(this._currentFilter);
                this._currentFilter = null;
                this._memberStack.Clear();
            }

            return b;
        }

        protected override Expression EvaluateMemberAccess(MemberExpression currentMember)
        {
            if (currentMember.Expression == null || currentMember.Expression.NodeType != ExpressionType.Parameter)
            {
                _memberStack.Push(currentMember);
                this.Evaluate(currentMember.Expression);
            }
            else
            {
                // use the mapping metadata and find the name of this memberInfo in the database
                Type tableType = currentMember.Member.DeclaringType.Name.Contains("Anonymous") ? currentMember.Type : currentMember.Member.DeclaringType;
                while (tableType.Name.Contains("Anonymous"))
                {
                    currentMember = _memberStack.Pop();
                    tableType = currentMember.Member.DeclaringType.Name.Contains("Anonymous") ? currentMember.Type : currentMember.Member.DeclaringType;
                }

                var table = _context.QueryableTableList.Where(tableType);
                var member = _memberStack.Count > 0 ? _memberStack.Pop().Member : currentMember.Member;
                _currentFilter.Field = table.Columns.Where(c => c.PropertyName == member.Name).FirstOrDefault();

                if (_currentFilter.Field == null)
                {
                    bool throwError = true;
                    if (member != currentMember.Member)
                    {
                        _currentFilter.Field = table.Columns.Where(c => c.PropertyName == currentMember.Member.Name).FirstOrDefault();
                        throwError = (_currentFilter.Field == null);
                    }

                    if (throwError) throw new InvalidOperationException(string.Format("The memberInfo {0} in where expression cannot be found on the {1} class", member.Name, tableType.Name));
                }
            }

            return currentMember;
        }

        protected override Expression EvaluateConstant(ConstantExpression c)
        {
            var value = c.Value;
            if (value is IQueryable) throw new NotSupportedException("Sub queries are not supported");

            if (_memberStack.Count > 0)
            {
                var member = _memberStack.Pop();
                if (value == null)
                {
                    throw new ArgumentException("Member " + member.Member.Name + " is null.");
                }

                do
                {
                    object memberValue = value;

                    if (member.Member.MemberType == MemberTypes.Field)
                    {
                        var field = (FieldInfo)value.GetType().GetMember(member.Member.Name).First();
                        memberValue = field.GetValue(value);

                        if (field.FieldType.IsEnum)
                        {
                            memberValue = Convert.ToInt16(memberValue);
                        }
                    }

                    if (_memberStack.Count > 0) member = _memberStack.Pop();
                    if (memberValue == null)
                    {
                        value = null;
                    }
                    else
                    {
                        var property = memberValue.GetType().GetProperty(member.Member.Name);
                        value = (property == null ? memberValue : property.GetValue(memberValue, null));
                    }

                } while (_memberStack.Count > 0);
            }

            _currentFilter.CompareValue = DbFunction.Instance.FormatValue(value);

            return c;
        }

        #endregion

        #region Static Method Members

        private static string[] SplitAndOrder(string value)
        {
            var splitValue = value.Split('|');
            if (splitValue.Length > 1)
            {
                var tableAlias = string.Empty;
                for (int i = 0; i < splitValue.Length; i++)
                {
                    if (string.IsNullOrEmpty(tableAlias))
                    {
                        tableAlias = splitValue[i].Split('.').FirstOrDefault() + ".";
                    }

                    if (!splitValue[i].StartsWith(tableAlias) && !string.IsNullOrEmpty(tableAlias))
                    {
                        splitValue[i] = tableAlias + splitValue[i];
                    }
                }
            }

            Array.Sort<string>(splitValue);

            return splitValue;
        }

        private static object GetConstantValue(ConstantExpression constantExp, string memberName)
        {
            var value = constantExp.Value;
            FieldInfo field = (FieldInfo)value.GetType().GetMember(memberName).First();

            return field.GetValue(value);
        }

        #endregion

        #region Method Members

        private object GetExpressionValue(Expression e)
        {
            if (e == null) return null;

            switch (e.NodeType)
            {
                case ExpressionType.Constant:
                    return (e as ConstantExpression).Value;
                case ExpressionType.MemberAccess:
                    return GetMemberValue(e as MemberExpression);
                case ExpressionType.Call:
                    return GetExpressionValue((e as MethodCallExpression).Arguments[0]);
                default:
                    throw new ArgumentException("NodeType " + e.NodeType.ToString() + " not implemented.");
            }
        }

        private object GetMemberValue(MemberExpression m)
        {
            var memberExp = m.Expression as MemberExpression;
            var constantExp = m.Expression as ConstantExpression;
            var methodExp = m.Expression as MethodCallExpression;

            if (memberExp != null)
            {
                _memberStack.Push(memberExp);
                var memberValue = GetMemberValue(m.Expression as MemberExpression);
                return memberValue is Expression ? m : (m.Member as PropertyInfo).GetValue(memberValue, null);
            }
            else if (methodExp != null)
            {
                return GetMethodResult(methodExp);
            }
            else if (constantExp != null)
            {
                return GetConstantValue(constantExp, _memberStack.Count == 0 ? m.Member.Name : _memberStack.Pop().Member.Name);
            }
            else if (m.Member.MemberType == MemberTypes.Field &&
                        m.Member.ToString() == "System.String Empty")
            {
                return string.Empty;
            }
            else
            {
                return m;
            }
        }

        private MethodCallExpression InterpretLike(MethodCallExpression currentMethod)
        {
            var instance = Evaluate(currentMethod.Object);
            _currentFilter.Operator = CompareOperators.Like;

            IEnumerable<Expression> arguments = EvaluateExpressionList(currentMethod.Arguments);
            if ((instance == currentMethod.Object) && (arguments == currentMethod.Arguments))
            {
                return currentMethod;
            }

            return Expression.Call(instance, currentMethod.Method, arguments);
        }

        private object GetMethodResult(MethodCallExpression currentMethod)
        {
            var arguments = new List<object>(currentMethod.Arguments.Count);
            for (int i = 0; i < currentMethod.Arguments.Count; i++)
            {
                var argValue = GetExpressionValue(currentMethod.Arguments[i]);
                arguments.Add(argValue);
            }

            return currentMethod.Method.Invoke(GetExpressionValue(currentMethod.Object), arguments.ToArray());
        }

        private MethodCallExpression InterpretMethodResult(MethodCallExpression m)
        {
            base.Evaluate(m.Object);

            var arguments = new List<object>(m.Arguments.Count);
            for (int i = 0; i < m.Arguments.Count; i++)
            {
                var argValue = GetExpressionValue(m.Arguments[i]);
                var argExpression = argValue as Expression;
                if (argExpression != null) //When coalesce receive the current table column as a parameter
                {
                    
                    return m;
                }

                arguments.Add(argValue);
            }

            var value = m.Method.Invoke(GetExpressionValue(m.Object), arguments.ToArray());

            if (value is PlainEntity)
            {
                var metaTable = MetaTableCache.GetItem(value.GetType());
                var pkValue = string.Empty;
                foreach (var property in metaTable.ColumnsList.WherePrimaryKey())
                {
                    pkValue += DbFunction.Instance.FormatValue(property.GetValue(value));
                    pkValue += "|";
                }

                _currentFilter.CompareValue = pkValue.Substring(0, pkValue.Length - 1);
            }
            else
            {
                _currentFilter.CompareValue = DbFunction.Instance.FormatValue(value);
            }

            _memberStack.Clear();

            return m;
        }

        private MethodCallExpression InterpretGetItem(MethodCallExpression m)
        {
            var methodResult = GetMethodResult(m);
            var member = _memberStack.Pop();
            var memberValue = (member.Member as PropertyInfo).GetValue(methodResult, null);

            _currentFilter.CompareValue = DbFunction.Instance.FormatValue(memberValue);

            return m;
        }

        private Expression InterpretNull(MethodCallExpression m)
        {
            Evaluate(m.Arguments.First());

            _currentFilter.Operator = m.Method.Name == "IsNull" ? CompareOperators.IsNull : CompareOperators.IsNotNull;

            return m;
        }

        private Expression InterpretCoalesce(MethodCallExpression m)
        {
            var compareValue = GetExpressionValue(m.Arguments[0]);
            var defaultValue = GetExpressionValue(m.Arguments[1]);
            var defaultExpression = defaultValue as MemberExpression;
            var nullValue = m.Arguments[2].NodeType == ExpressionType.MemberAccess 
                ? GetMemberValue(m.Arguments[2] as MemberExpression)
                : (m.Arguments[2] as ConstantExpression).Value;

            if ((compareValue == null && defaultExpression != null) ||
                (compareValue.Equals(nullValue)))
            {
                _currentFilter = null;
                return m;
            }
            else if (compareValue == null && defaultExpression == null)
            {
                CoalesceValue(defaultValue);
            }
            else if (compareValue != null)
            {
                CoalesceValue(compareValue);
            }

            return m;
        }

        private void CoalesceValue(object defaultValue)
        {
            var entity = defaultValue as PlainEntity;
            if (entity == null)
            {
                _currentFilter.CompareValue = DbFunction.Instance.FormatValue(defaultValue);
            }
            else
            {
                var property = defaultValue.GetType().GetProperty(_currentFilter.Field.PropertyName);
                if (property != null)
                {
                    _currentFilter.CompareValue = DbFunction.Instance.FormatValue(property.GetValue(defaultValue, null));
                }
                else
                {
                    var isFirst = true;
                    var table = _context.QueryableTableList.Where(defaultValue.GetType());
                    var metaTable = MetaTableCache.GetItem(defaultValue.GetType());
                    foreach (var pkCcolumn in metaTable.ColumnsList.WherePrimaryKey())
                    {
                        if (isFirst)
                        {
                            _currentFilter = new CoalesceToken()
                                {
                                    Field = _currentFilter.Field,
                                    Operator = _currentFilter.Operator,
                                    CombineOperator = _currentFilter.CombineOperator,
                                    CustomFieldName = pkCcolumn.DBColumn.Name,
                                    CompareValue = DbFunction.Instance.FormatValue(pkCcolumn.GetValue(entity))
                                };
                            isFirst = false;
                        }
                        else
                        {
                            _whereStatement.PredicateList.Add(new CoalesceToken()
                            {
                                Operator = CompareOperators.Equals,
                                CombineOperator = CompareOperators.AndAlso,
                                CompareValue = DbFunction.Instance.FormatValue(pkCcolumn.GetValue(entity)),
                                CustomFieldName = pkCcolumn.DBColumn.Name,
                                Field = _currentFilter.Field                                
                            });
                        }
                    }
                }
            }
        }

        #endregion
    }
}
